package ssau.knyazev.fft.modules;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

import ssau.knyazev.core.B2Complex;
import ssau.knyazev.core.CComplex;
import ssau.knyazev.core.Complex;
import ssau.knyazev.fft.common.FFTConst;
import ssau.knyazev.fft.common.FFTRuntimeException;
import ssau.knyazev.fft.modules.threads.B2ComplexAlgThread;
import ssau.knyazev.fft.modules.threads.B2ComplexThread;
import ssau.knyazev.fft.modules.threads.CComplexMirrorThread;
import ssau.knyazev.fft.modules.threads.CComplexThread;
import ssau.knyazev.fft.modules.threads.ComplexThread;
import ssau.knyazev.fft.modules.threads.RowColThread;
import ssau.knyazev.modules.Generator;
import ssau.knyazev.modules.Helper;
import ssau.knyazev.modules.Printer;

public class CPUFFTModule
{

	protected int blockX;
	protected int blockY;
	protected Complex[][] data = null;
	protected CComplex[][] cdata = null;
	protected B2Complex[][] b2data = null;
	protected Thread[][] threads = null;
	protected CyclicBarrier barrier = null;
	protected ThreadGroup threadPool = null;
	protected int size;
		
	protected CPUFFTModule(int blockY, int blockX, Complex[][] arr){
		this.blockY = blockY;
		this.blockX = blockX;
		this.data = arr;
		threads = new Thread[blockY][blockX];
		barrier = new CyclicBarrier(blockY * blockX);
		threadPool = new ThreadGroup("X");
	}
	
	public static Complex[][] fft(float[][] arr, int mode, int blockX, int blockY) throws FFTRuntimeException{
		Complex[][] data = Helper.transformFloat(arr);
		return fft(data, mode, blockX, blockY);
	}
	
	public static Complex[][] fft(Complex[][] data, int mode, int blockX, int blockY) throws FFTRuntimeException{
		CPUFFTModule core = new CPUFFTModule(blockY, blockX, data);
		switch (mode) {
			case FFTConst.ROWCOL_MODE:
				return core.fft2DSimpleRowCol();
			case FFTConst.SIMPLE_MODE:
				return core.fft2DSimple();
			case FFTConst.SIMPLE_SYNC_MODE:
				return core.fft2DSimpleSync();
			case FFTConst.CCOMPLEX_SIMPLE_MODE:
				return core.fft2DCComplex(data);
			case FFTConst.CCOMPLEX_MIRROR_MODE:
				return core.fft2DCComplexMirror(data);
			case FFTConst.B2COMPLEX_DECOMPOSE_BY_ALG:
				core.barrier = new CyclicBarrier(2);
				return core.fft2DB2ComplexByAlgebra();
			case FFTConst.B2COMPLEX_SIMPLE_MODE:
				return core.fft2DB2Complex();
			default:
				throw new FFTRuntimeException("This mode is not supported " + mode);
		}
	}
	
	private Complex[][] fft2DSimpleRowCol(){
		Complex[] wlist = new Complex[data.length];
		for (int i = 0; i < wlist.length; i++){
			wlist[i] = Complex.W(data.length, i);			
		}
		inverse(data);
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				Runnable target = new RowColThread(barrier, x, y, blockX, data, wlist);
				threads[y][x] = new Thread(threadPool, target);
				threads[y][x].setPriority(Thread.MAX_PRIORITY);
			}
		}
		
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				threads[y][x].start();
			}
		}
		
		Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
		while (threadPool.activeCount() != 0){
		}
		Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
		return data;
	}
	
	private Complex[][] fft2DSimple(){
		Complex[] wlist = new Complex[data.length];
		for (int i = 0; i < wlist.length; i++){
			wlist[i] = Complex.W(data.length, i);			
		}
		inverse(data);
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				Runnable target = new ComplexThread(barrier, x, y, blockX, blockY, data, wlist);
				threads[y][x] = new Thread(threadPool, target);
				threads[y][x].setPriority(Thread.MAX_PRIORITY);
			}
		}
		
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				threads[y][x].start();
			}
		}
		
		Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
		while (threadPool.activeCount() != 0){
		}
		Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
		//System.out.println("fftSimple finished");
		return data;
	}
	
	private Complex[][] fft2DSimpleSync(){
		Complex[] wlist = new Complex[data.length];
		for (int i = 0; i < wlist.length; i++){
			wlist[i] = Complex.W(data.length, i);			
		}
		inverse(data);
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				Runnable target = new ComplexThread(barrier, x, y, blockX, blockY, data, wlist);
				threads[y][x] = new Thread(threadPool, target);
				threads[y][x].setPriority(Thread.MAX_PRIORITY);
			}
		}
		
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				threads[y][x].start();
			}
		}
		
		Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
		while (threadPool.activeCount() != 0){
		}
		Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
		return data;
	}
	
	
	private Complex[][] fft2DB2ComplexByAlgebra(){
		int N = data.length;
		B2Complex[] wilist = new B2Complex[N];
		B2Complex[] wjlist = new B2Complex[N];
		float cos, sin;
		for (int i = 0; i < N; i++) {
			double power = -2*Math.PI*i/N;
			cos = (float)Math.cos(power);
			sin = (float)Math.sin(power);
			wilist[i] = new B2Complex(cos/2, cos/2, sin/2, sin/2);
			wjlist[i] = new B2Complex(cos/2, cos/2, -sin/2, sin/2);
		}
		b2data = fromComplexToB2Complex(data);
		inverse(b2data);
		
		Runnable target = new B2ComplexAlgThread(b2data, wilist, wjlist, true);
		Thread thread = new Thread(threadPool, target);
		thread.setPriority(Thread.MAX_PRIORITY);
		thread.start();
		
		target = new B2ComplexAlgThread(b2data, wilist, wjlist, false);
		thread = new Thread(threadPool, target);
		thread.setPriority(Thread.MAX_PRIORITY);
		thread.start();
		
		Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
		while (threadPool.activeCount() != 0){
		}
		Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
		//data = convertToComplex(cdata);
		
		return data;
	}
	
	private Complex[][] fft2DB2Complex(){
		int N = data.length;
		B2Complex[] wilist = new B2Complex[N];
		B2Complex[] wjlist = new B2Complex[N];
		float cos, sin;
		for (int i = 0; i < N; i++) {
			double power = -2*Math.PI*i/N;
			cos = (float)Math.cos(power);
			sin = (float)Math.sin(power);
			wilist[i] = new B2Complex(cos/2, cos/2, sin/2, sin/2);
			wjlist[i] = new B2Complex(cos/2, cos/2, -sin/2, sin/2);
		}
		b2data = fromComplexToB2Complex(data);
		inverse(b2data);
		
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				Runnable target = new B2ComplexThread(barrier, x, y, blockX, blockY, b2data, wilist, wjlist);
				threads[y][x] = new Thread(threadPool, target);
				threads[y][x].setPriority(Thread.MAX_PRIORITY);
			}
		}
		
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				threads[y][x].start();
			}
		}
		
		Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
		while (threadPool.activeCount() != 0){
		}
		Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
		//data = convertToComplex(cdata);
		
		return data;
	}

	/*
	public static Complex[][] fft2DB2ComplexByDnA(float[][] src){
		
		return null;
	}
	
	public static Complex[][] fft2DB2ComplexByDnA(float[][] src){
		
		return null;
	}
	*/
	

	
	private Complex[][] fft2DCComplex(Complex[][] src){
		int N = src.length;
		CComplex[] wilist = new CComplex[src.length];
		CComplex[] wjlist = new CComplex[src.length];
		for (int i = 0; i < src.length; i++){
			wilist[i] = CComplex.W_I(N, i);
			wjlist[i] = CComplex.W_J(N, i);
		}
		cdata = fromComplexToCComplex(src);
		inverse(cdata);
		
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				Runnable target = new CComplexThread(barrier, x, y, blockX, blockY, cdata, wilist, wjlist);
				threads[y][x] = new Thread(threadPool, target);
				threads[y][x].setPriority(Thread.MAX_PRIORITY);
			}
		}
		
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				threads[y][x].start();
			}
		}
		
		Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
		while (threadPool.activeCount() != 0){
		}
		Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
		//data = convertToComplex(cdata);
		
		return data;
	}
	
	private Complex[][] fft2DCComplexMirror(Complex[][] src){
		int N = src.length;
		CComplex[] wilist = new CComplex[src.length];
		CComplex[] wjlist = new CComplex[src.length];
		for (int i = 0; i < src.length; i++){
			wilist[i] = CComplex.W_I(N, i);
			wjlist[i] = CComplex.W_J(N, i);
		}
		cdata = fromComplexToCComplex(src);
		inverse(cdata);
		
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				Runnable target = new CComplexMirrorThread(barrier, x, y, blockX, blockY, cdata, wilist, wjlist);
				threads[y][x] = new Thread(threadPool, target);
				threads[y][x].setPriority(Thread.MAX_PRIORITY);
			}
		}
		
		for (int y = 0; y < threads.length; y++){
			for (int x = 0; x < threads[y].length; x++){
				threads[y][x].start();
			}
		}
		Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
		while (threadPool.activeCount() != 0){
		}
		Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
		//data = convertToComplex(cdata);
		
		return data;
	}
	
	private static CComplex[][] fromComplexToCComplex(Complex[][] in) {
		CComplex[][] res = new CComplex[in.length][in.length];
		for (int i = 0; i < in.length; i++)
			for (int j = 0; j < in.length; j++) {
				res[i][j] = new CComplex(in[i][j].getReal(), 0, 0, 0);
			}
		return res;
	}
	
	private static B2Complex[][] fromComplexToB2Complex(Complex[][] in) {
		B2Complex[][] res = new B2Complex[in.length][in.length];
		for (int i = 0; i < in.length; i++)
			for (int j = 0; j < in.length; j++) {
				res[i][j] = new B2Complex(in[i][j].getReal()/2, in[i][j].getReal()/2, in[i][j].getImg()/2, in[i][j].getImg()/2);
			}
		return res;
	}
	
	private static Complex[][] convertToComplex(CComplex[][] arr) {
		Complex[][] res = new Complex[arr.length][arr.length];
		for (int i = 0; i < res.length; i++){
			for (int j = 0; j < res.length; j++){
				res[i][j] = arr[i][j].getComplex();
			}
		}
		return res;
	}
	
	private static void inverse(Object[] arr){
		int dl = arr.length/2;
		int st = arr.length - 1;
		int j = 0;
		for (int i = 0; i < st; i++){
			if (i < j){
				Object s0 = arr[i];
				arr[i] = arr[j];
				arr[j] = s0;
			}
			int k = dl;
			while (k <= j){
				j = j - k;
				k = k >> 1;
			}
			j = j + k;
		}
	}
	
	private static void inverse(Object[][] src){
		int N = src.length;
		for (int i = 0; i < N; i++){
			inverse(src[i]);
		}
		Object[] arr = new Object[N];
		for (int i = 0; i < N; i++){
			for (int j = 0; j < N; j++){
				arr[j] = src[j][i];
			}
			inverse(arr);
			for (int j = 0; j < N; j++){
				src[j][i] = arr[j];
			}
		}
	}
	
	public static void main(String[] args) throws FFTRuntimeException
	{
		float[][] arr = Generator.generateSimpleMatrix(2048);
		long sum = 0;
		long time = 0;
		Complex[][] data = Helper.transformFloat(arr);
		//Printer.printVector(arr);
		for (int i = 0; i < 10; i++){
			System.out.println(i);
			time = System.nanoTime();
			Complex[][] res = CPUFFTModule.fft(data, FFTConst.B2COMPLEX_SIMPLE_MODE, 2, 2);
			time = System.nanoTime() - time;
			//Printer.printVector(res);
			System.out.println(i + ": " + time);
			sum += time;
		}
		time = sum/10;
		System.out.println(": " + time);
	}
	
}
